/*******************************************************************************
 * 作者:    水煮肉片饭(27185709@qq.com)
 * 版本:    v2.3
 * 描述:    运动拖尾
 * 1、支持合批
 *    可以使用Atlas的图集帧作为拖尾贴图，不打断DrawCall
 * 2、支持世界坐标 与 节点坐标
 *    旧版拖尾只支持世界坐标，如果游戏中采用“反向移动地图”来实现视窗跟随，由于世界坐标并没有改变，会导致拖尾异常。
 *    新版拖尾支持世界坐标 和 本地坐标 两种模式切换，以上问题使用本地坐标就可以解决。
 * 3、避免内存波动
 *    原版拖尾通过不断创建渐隐顶点来模拟拖尾效果（类似于粒子系统）。
 *    新版拖尾的实现原理完全遵从拖尾的本质，顶点数是固定的，通过更新顶点位置来改变尾巴形状，内存更稳定。
 * 4、其他功能
 *    节点缩放，拖尾会跟着缩放。节点旋转，拖尾不会跟着旋转。
 *    上级节点缩放，拖尾不会跟着缩放，但可以调用updateCascadeScale函数让拖尾同步缩放。
*******************************************************************************/
class Joint {           //尾巴关节，每个关节控制2个顶点，N个关节组成尾巴骨骼（N = 尾巴长度）
    x: number = 0;      //关节x
    y: number = 0;      //关节y
    dis: number = 0;    //与顶点的距离（一个关节沿切线方向左右各延伸dis距离，就是2个顶点坐标）
    cos: number = 0;    //关节弯曲角cos值
    sin: number = 0;    //关节弯曲角sin值
}
const { ccclass, property, playOnFocus, menu } = cc._decorator;
@ccclass
@playOnFocus
@menu('Comp/MotionTrail')
export default class MotionTrail extends cc.RenderComponent {
    @property({ type: cc.SpriteAtlas, serializable: false, readonly: true, displayName: CC_DEV && 'Atlas' })
    private atlas: cc.SpriteAtlas = null;
    @property
    private _spriteFrame: cc.SpriteFrame = null;
    @property({ type: cc.SpriteFrame, displayName: CC_DEV && 'Sprite Frame' })
    get spriteFrame() { return this._spriteFrame; }
    set spriteFrame(value: cc.SpriteFrame) {
        this._spriteFrame = value;
        this.updateSpriteFrame();
        this.updateUV();
    }
    @property
    private _isActive: boolean = true;
    @property({ displayName: CC_DEV && '是否激活', tooltip: CC_DEV && '设置isActive = true可以刷新顶点数据' })
    get isActive() { return this._isActive; }
    set isActive(value: boolean) {
        this._isActive = this.enabled = value;
        this.updateActive();
    }
    @property
    private _isWorldXY: boolean = true;
    @property({ displayName: CC_DEV && '世界坐标', tooltip: CC_DEV && '顶点坐标是世界坐标还是本地坐标' })
    get isWorldXY() { return this._isWorldXY; }
    set isWorldXY(value: boolean) {
        this._isWorldXY = value;
        this.resetPos();
    }
    @property({ displayName: CC_DEV && '偏移' })
    offset: cc.Vec2 = cc.v2(0, 0);
    @property
    private _length: number = 20;
    @property({ type: cc.Integer, displayName: CC_DEV && '拖尾长度' })
    get length() { return this._length; }
    set length(value: number) {
        this._length = Math.max(value, 2);
        this.updateLength();
        this.updateIndice();
        this.updateUV();
        this.updateWidth();
        this.resetPos();
        this.node['_renderFlag'] |= cc['RenderFlow'].FLAG_OPACITY_COLOR;
    }
    @property
    private _headWidth: number = 100;
    @property({ displayName: CC_DEV && '头部宽度' })
    get headWidth() { return this._headWidth; }
    set headWidth(value: number) {
        this._headWidth = Math.max(value, 0);
        this.updateWidth();
    }
    @property
    private _tailWidth: number = 0;
    @property({ displayName: CC_DEV && '尾部宽度' })
    get tailWidth() { return this._tailWidth; }
    set tailWidth(value: number) {
        this._tailWidth = Math.max(value, 0);
        this.updateWidth();
    }
    @property
    private _headOpacity: number = 255;
    @property({ type: cc.Integer, min: 0, max: 255, slide: true, displayName: CC_DEV && '头部透明度' })
    get headOpacity() { return this._headOpacity; }
    set headOpacity(value: number) {
        this._headOpacity = value;
        this.node['_renderFlag'] |= cc['RenderFlow'].FLAG_OPACITY_COLOR;
    }
    @property
    private _tailOpacity: number = 0;
    @property({ type: cc.Integer, min: 0, max: 255, slide: true, displayName: CC_DEV && '尾部透明度' })
    get tailOpacity() { return this._tailOpacity; }
    set tailOpacity(value: number) {
        this._tailOpacity = value;
        this.node['_renderFlag'] |= cc['RenderFlow'].FLAG_OPACITY_COLOR;
    }
    private renderData = null;                      //提交给GPU的渲染数据，包括vDatas、uintVDatas、iDatas
    private xyOffset: number = 1e8;                 //顶点坐标数据，在顶点数组中的偏移
    private uvOffset: number = 1e8;                 //顶点uv数据，在顶点数组中的偏移
    private colorOffset: number = 1e8;              //顶点颜色数据，在顶点数组中的偏移
    private step: number = 0;                       //单个顶点数据的长度，例如：顶点格式“x,y,u,v,color” step = 5
    private joints: Joint[] = [];                   //尾巴关节组
    private cascadeScale: cc.Vec2 = cc.v2(1, 1);    //级联缩放，即所有上级节点缩放的乘积（上级节点指父节点、父节点的父节点……）
    private visibleScale: cc.Vec2 = cc.v2(1, 1);    //可见缩放，即级联缩放 * 节点自身缩放

    protected _resetAssembler() {
        //定制Assembler
        let assembler = this['_assembler'] = new cc['Assembler']();
        assembler.updateRenderData = this.updateXY.bind(this);
        assembler.updateColor = this.updateColor.bind(this);
        this.update = this['setVertsDirty'];
        assembler.init(this);
        //定制RenderData
        this.renderData = new cc['RenderData']();
        this.renderData.init(assembler);
        //初始化顶点格式
        let vfmt = assembler.getVfmt();
        let fmtElement = vfmt._elements;
        for (let i = fmtElement.length - 1; i > -1; this.step += fmtElement[i--].bytes >> 2);
        let fmtAttr = vfmt._attr2el;
        this.xyOffset = fmtAttr.a_position.offset >> 2;
        this.uvOffset = fmtAttr.a_uv0.offset >> 2;
        this.colorOffset = fmtAttr.a_color.offset >> 2;
    }

    protected onLoad() {
        this.updateSpriteFrame();
        this.updateLength();
        this.updateIndice();
        this.updateUV();
        this.updateWidth();
        this.updateActive();
        cc.sys.isNative && cc.director.once(cc.Director.EVENT_AFTER_DRAW, this.updateColor, this);
        this.node.on(cc.Node.EventType.SCALE_CHANGED, this.updateVisibleScale, this);
    }

    protected onDestroy() {
        this.node.off(cc.Node.EventType.SCALE_CHANGED, this.updateVisibleScale, this);
    }
    //设置enable为true，不但可以显示拖尾，还可以重置拖尾位置，并同步上级节点缩放对拖尾造成的影响。
    protected updateActive() {
        if (this._isActive) {
            this.updateCascadeScale();
            this.updateVisibleScale();
            this.resetPos();
        }
    }
    //设置顶点个数和三角形个数
    private createBuffer(verticesCount: number, triangleCount: number = verticesCount - 2) {
        let vertices = new Float32Array(this.step * verticesCount);
        let indices = new Uint16Array(3 * triangleCount);
        this.renderData.updateMesh(0, vertices, indices);
    }
    //Web平台，将renderData的数据提交给GPU渲染，vDatas使用世界坐标
    //原生平台并不会执行该函数，引擎另外实现了渲染函数，vDatas使用本地坐标
    private fillBuffers(comp: cc.RenderComponent, renderer: any) {
        let vData = this.renderData.vDatas[0];
        let iData = this.renderData.iDatas[0];
        let buffer = renderer._meshBuffer;
        let offsetInfo = buffer.request(vData.length, iData.length);
        let vertexOffset = offsetInfo.byteOffset >> 2;
        let vbuf = buffer._vData;
        if (vData.length + vertexOffset > vbuf.length) {
            vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset);
        } else {
            vbuf.set(vData, vertexOffset);
        }
        let ibuf = buffer._iData;
        let indiceOffset = offsetInfo.indiceOffset;
        let vertexId = offsetInfo.vertexOffset;
        for (let i = 0, len = iData.length; i < len; ibuf[indiceOffset++] = vertexId + iData[i++]);
    }
    //可以传入cc.SpriteFrame图集帧（支持合批，推荐），或单张图片cc.Texture2D
    private updateSpriteFrame() {
        let frame = this._spriteFrame;
        this['_assembler'].fillBuffers = frame ? this.fillBuffers.bind(this) : () => { };
        let material = this.getMaterial(0) || cc.Material.getBuiltinMaterial('2d-sprite');
        material.define("USE_TEXTURE", true);
        material.setProperty("texture", frame ? frame.getTexture() : null);
        if (CC_EDITOR) {
            if (frame && frame.isValid && frame['_atlasUuid']) {
                cc.assetManager.loadAny(frame['_atlasUuid'], (err, asset: cc.SpriteAtlas) => {
                    this.atlas = asset;
                });
            } else {
                this.atlas = null;
            }
        }
    }
    //计算级联缩放，也就是所有上级节点缩放系数的乘积
    private updateCascadeScale() {
        let node = this.node;
        let sx = 1, sy = 1;
        while (node['_parent'] !== null) {
            node = node['_parent'];
            sx *= node.scaleX;
            sy *= node.scaleY;
        }
        this.cascadeScale.x = sx;
        this.cascadeScale.y = sy;
    }
    //计算可见缩放
    private updateVisibleScale() {
        this.visibleScale.x = this.cascadeScale.x * this.node.scaleX;
        this.visibleScale.y = this.cascadeScale.y * this.node.scaleY;
    }
    //计算关节数据以及顶点坐标（每个关节控制2个顶点）
    protected updateXY() {
        let node = this.node;
        let joints = this.joints;
        for (let i = this._length - 1; i > 0; --i) {
            let cur = joints[i], prev = joints[i - 1];
            cur.x = prev.x;
            cur.y = prev.y;
            cur.sin = prev.sin;
            cur.cos = prev.cos;
        }
        let cur = joints[0], next = joints[1];
        let vsx = this.visibleScale.x, vsy = this.visibleScale.y;
        if (this._isWorldXY) {
            let m = node['_worldMatrix'].m;
            cur.x = this.offset.x * vsx + m[12];
            cur.y = this.offset.y * vsy + m[13];
        } else {
            cur.x = this.offset.x * node.scaleX + node.x;
            cur.y = this.offset.y * node.scaleY + node.y;
        }
        let a = next.y - cur.y, b = next.x - cur.x, sqrt = Math.sqrt(a * a + b * b);
        cur.sin = a / sqrt;
        cur.cos = b / sqrt;
        let vData = this.renderData.vDatas[0];
        let tx = 0, ty = 0, csx = 1, csy = 1;
        if (!this._isWorldXY) {
            tx = node.x, ty = node.y;
            csx = this.cascadeScale.x, csy = this.cascadeScale.y;
        }
        let id = 0;
        let step = this.step;
        let curX = 0, curY = 0, disX = 0, disY = 0;
        for (let i = 0, len = this._length - 1; i < len; ++i) {
            cur = joints[i];
            next = joints[i + 1];
            curX = (cur.x - tx) * csx;
            curY = (cur.y - ty) * csy;
            disX = cur.dis * vsx * cur.sin;
            disY = cur.dis * vsy * cur.cos;
            vData[id] = curX + disX;
            vData[id + 1] = curY - disY;
            id += step;
            vData[id] = curX - disX;
            vData[id + 1] = curY + disY;
            id += step;
        }
        curX = (next.x - tx) * csx;
        curY = (next.y - ty) * csy;
        disX = next.dis * vsx * cur.sin;
        disY = next.dis * vsy * cur.cos;
        vData[id] = curX + disX;
        vData[id + 1] = curY - disY;
        id += step;
        vData[id] = curX - disX;
        vData[id + 1] = curY + disY;
        this.fitXY(vData);
    }
    //初始化顶点数据
    private resetPos() {
        let tx = this.offset.x, ty = this.offset.y;
        let node = this.node;
        if (this._isWorldXY) {
            let m = node['_worldMatrix'].m;
            node['_updateWorldMatrix']();
            tx += m[12];
            ty += m[13];
        } else {
            tx += node.x;
            ty += node.y;
        }
        for (let i = this._length - 1, joints = this.joints; i > -1; --i) {
            joints[i].x = tx;
            joints[i].y = ty;
        }
        let vData = this.renderData.vDatas[0];
        for (let i = this.xyOffset, len = vData.length, step = this.step; i < len; i += step) {
            vData[i] = tx;
            vData[i + 1] = ty;
        }
        this.fitXY(vData);
    }
    //自动适配XY，修改顶点xy数据后需主动调用该函数
    private fitXY(vData: Float32Array) {
        if (cc.sys.isNative) {//原生平台默认会进行一次本地坐标 到 世界坐标的转换
            this.node['_updateWorldMatrix']();//因此先用逆矩阵抵消该转换，否则拖尾会受节点旋转影响
            let im = cc.Mat4.invert(new cc.Mat4(), this.node['_worldMatrix']).m;
            let im0 = im[0], im1 = im[1], im4 = im[4], im5 = im[5], tx = 0, ty = 0;
            if (this._isWorldXY) {
                tx = im[12];
                ty = im[13];
            }
            for (let i = this.xyOffset, len = vData.length; i < len; i += this.step) {
                let x = vData[i], y = vData[i + 1];
                vData[i] = im0 * x + im4 * y + tx;
                vData[i + 1] = im1 * x + im5 * y + ty;
            }
        } else if (!this._isWorldXY) {//Web平台需手动将本地坐标转换到世界坐标
            let m = this.node['_worldMatrix'].m;
            let tx = m[12], ty = m[13];
            for (let i = this.xyOffset, len = vData.length; i < len; i += this.step) {
                vData[i] = vData[i] + tx;
                vData[i + 1] = vData[i + 1] + ty;
            }
        }
    }
    //根据关节数，均匀分布顶点UV
    private updateUV() {
        let vData = this.renderData.vDatas[0];
        let step = this.step;
        let uvStep = 1 / (this.joints.length - 1);
        for (let i = this.uvOffset, id = 0, len = vData.length; i < len; i += step, ++id) {
            vData[i] = id & 1;
            vData[i + 1] = 1 - uvStep * (id >> 1);
        }
        this.fitUV(vData);
    }
    //自动适配UV，修改顶点uv数据后需主动调用该函数
    private fitUV(vData: Float32Array) {
        let frame = this._spriteFrame;
        if (frame === null) return;
        let atlasW = frame['_texture'].width, atlasH = frame['_texture'].height;
        let frameRect = frame['_rect'];
        //计算图集帧在大图中的UV坐标
        if (frame['_rotated']) {//如果图集帧发生旋转，计算UV时需回正
            for (let i = this.uvOffset, id = 0, len = vData.length, step = this.step; i < len; i += step, ++id) {
                let tmp = vData[i];
                vData[i] = ((1 - vData[i + 1]) * frameRect.height + frameRect.x) / atlasW;
                vData[i + 1] = (tmp * frameRect.width + frameRect.y) / atlasH;
            }
        } else {//如果图集帧未发生旋转，正常计算即可
            for (let i = this.uvOffset, id = 0, len = vData.length, step = this.step; i < len; i += step, ++id) {
                vData[i] = (vData[i] * frameRect.width + frameRect.x) / atlasW;
                vData[i + 1] = (vData[i + 1] * frameRect.height + frameRect.y) / atlasH;
            }
        }
    }
    //计算顶点颜色
    private updateColor() {
        let trailLen = this._length;
        let headOpa = this._headOpacity;
        let opaDelt = (headOpa - this._tailOpacity) / (trailLen - 1);
        let opaRatio = this.node.color.a / 255;
        let rgb = this.node.color['_val'] & 0x00FFFFFF;
        let uintVData = this.renderData.uintVDatas[0];
        for (let i = 0, id = this.colorOffset, step = this.step; i < trailLen; ++i) {
            let color = (headOpa - opaDelt * i) * opaRatio << 24 | rgb;
            uintVData[id] = color;
            id += step;
            uintVData[id] = color;
            id += step;
        }
    }
    //计算顶点索引
    private updateIndice() {
        let iData = this.renderData.iDatas[0];
        for (let i = 0, id = 0, len = iData.length; i < len; ++id) {
            iData[i++] = id;
            iData[i++] = id + 1;
            iData[i++] = id + 2;
        }
    }
    //根据尾巴长度、顶点格式（X|Y|U|V|Color）分配顶点数据所需的内存
    private updateLength() {
        let trailLen = this._length;
        for (let i = 0, joints = this.joints = []; i < trailLen; joints[i++] = new Joint());
        this.createBuffer(trailLen << 1);
    }
    //根据头部宽度、尾部宽度计算关节与顶点的距离（插值法）
    private updateWidth() {
        let trailLen = this._length;
        let headHalfW = this._headWidth * 0.5;
        let disDelt = (headHalfW - this._tailWidth * 0.5) / (trailLen - 1);
        for (let i = 0, joints = this.joints; i < trailLen; joints[i].dis = headHalfW - disDelt * i++);
    }
}